﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;
using System.Threading;
using CILADA.Engine;
using CILADA.Engine.DynamicDependencyScanners;
using log4net;
using NDesk.Options;
using DDSUtils = CILADA.Engine.DynamicDependencyScanners.AnalysisUtils;

namespace CILADA.CommandLine
{
    class Program
    {
        #region Fields
        private static readonly ILog log = LogManager.GetLogger(typeof(Program));
        #endregion

        static void Main(string[] args)
        {
            FileInfo execAssem = new FileInfo(Assembly.GetExecutingAssembly().Location);
            string defaultOuputPath = Path.Combine(execAssem.DirectoryName, CompileTimeDependencyAnalyser.DEFAULT_OUTPUT_DIR);

            log.Debug("Default output path is " + defaultOuputPath);

            #region Option Parsing
            bool show_help = false;
            bool pauseAtCompletion = false;
            string outputPath = String.Empty;
            string outputFileName = String.Empty;
            List<string> directories = new List<string>();
            bool ignoreExe = false;
            bool ignoreDll = false;
            bool recursive = false;
            bool filePerAssembly = false;
            bool runDynamicAnalysis = false;
            bool verbose = false;

            var p = new OptionSet() {
            { "o|outdir=", "the full path to a directory to put the output in. It will be created if it does not exist.",
              v => outputPath = v },

            { "n|outname=", "specifies the name the output file will have. Note that some formatters add their own extension, such as .graphml to this name",
              v => outputFileName = v },

            { "p|pause", "the program will require a key press to close the command prompt once it has finished executing.",
              v => pauseAtCompletion = v != null },

            { "f|fileperassembly", "Each assembly will be analysed by itself, and will have an output file, rather than the default of merging all the assemblies analysis into one output file.",
              v => filePerAssembly = v != null },

            { "d|dir=", "a full directory path. Will add all the assemblies in it to the analysis, if the assembly type is not being ignored.",
              v => directories.Add(v) },

            { "r|recursive", "any directories specified will be recursively searched for assemblies",
              v => recursive = true },

            { "i|ignore=", "used in conjunction with dir. takes either dll or exe, and will ignore all of the given type in all directories specified. If one type is ignored, the other type will not be.",
              v => ProcessIgnore(v, ref ignoreDll, ref ignoreExe) },

            { "a|dynamicanalysis", "run a search for possible dynamic dependencies on each assembly as well as looking for static dependencies.",
              v => runDynamicAnalysis = true },

            { "v|verbose", "adds updates to the console whenever an assembly has finished being analysed.",
              v => verbose = true },

            { "h|help",  "show this message and exit",
              v => show_help = v != null },
            };

            Console.WriteLine();

            HashSet<string> assemblyPaths = new HashSet<string>();
            try
            {
                List<string> singlePaths = p.Parse(args);
                foreach (string s in singlePaths)
                {
                    assemblyPaths.Add(s);
                }
            }
            catch (OptionException oe)
            {
                Console.WriteLine("Invalid option or parameter on " + oe.OptionName);
                Console.WriteLine("Try 'CILADA.CommandLine --help' for more information.");
                return;
            }

            log.Info(assemblyPaths.Count + " assemblies added for analysis.");
            List<string> filesFromDirs = ProcessDirectories(directories, recursive, ignoreDll, ignoreExe);
            log.Info(filesFromDirs.Count + " assemblies added for analysis from directories");
            assemblyPaths.UnionWith(filesFromDirs);

            if (show_help)
            {
                ShowHelp(p);
                if (pauseAtCompletion)
                {
                    Console.WriteLine("Press any key to quit...");
                    Console.ReadKey(true);
                }
                return;
            }

            IDictionary<string, string> assemblyNamesAndPaths = new Dictionary<string, string>();
            if (0 == assemblyPaths.Count)
            {
                Console.WriteLine("\tNo assemblies specified");
                Console.WriteLine();
                ShowHelp(p);
                if (pauseAtCompletion)
                {
                    Console.WriteLine("Press any key to quit...");
                    Console.ReadKey(true);
                }
                return;
            }
            else
            {
                // remove any duplicates of assembly names, even if the paths are different.
                // TODO: work out how to handle the case of multiple assemblies with the same names
                // with different paths. Currently, it just ignores any names that have multiple paths.
                IDictionary<string, List<string>> duplicates = new Dictionary<string, List<string>>();
                string name = String.Empty;
                foreach (string path in assemblyPaths)
                {
                    name = Path.GetFileName(path);
                    if (duplicates.ContainsKey(name))
                    {
                        duplicates[name].Add(path);
                    }
                    else
                    {
                        List<string> dupListPaths = new List<string>();
                        dupListPaths.Add(path);
                        duplicates.Add(name, dupListPaths);
                    }
                }

                bool headerWritten = false;
                foreach (KeyValuePair<string, List<string>> kvp in duplicates)
                {
                    if (kvp.Value.Count.Equals(1))
                    {
                        assemblyNamesAndPaths.Add(kvp.Key, kvp.Value[0]);
                    }
                    else
                    {
                        if (!headerWritten) Console.WriteLine("# Duplicate assembly names found:");
                        Console.WriteLine("\t" + kvp.Key);
                        foreach (string ps in kvp.Value)
                        {
                            Console.WriteLine("\t  ->" + ps);
                        }
                    }
                }
            }

            if (String.Empty.Equals(outputFileName))
            {
                DateTime now = DateTime.Now;
                outputFileName = "AssemblyAnalysisResult-" + now.Year + "-" + now.DayOfYear + "-" + now.Hour + "-" + now.Minute + "-" + now.Second;

                Console.WriteLine("No file name given for the analysis result. Setting the name to " + outputFileName);
            }

            // validate the path
            if (String.Empty.Equals(outputPath))
            {
                // user didnt enter anything, so set to default.
                outputPath = defaultOuputPath;
                log.Debug("No output path set by user, using default of " + defaultOuputPath);
                Console.WriteLine("No output path set by user, using default of " + defaultOuputPath);
            }
            else if (!ValidateUserOutputPath(outputPath))
            {
                log.Info("Invalid output path " + outputPath + " set by user, using default.");
                Console.WriteLine("Invalid output path " + outputPath);
                outputPath = defaultOuputPath;
                Console.WriteLine("Output path is set to " + outputPath);
            }
            #endregion

            #region Static Dependency Analysis
            CompileTimeDependencyAnalyser CTAssemAnalyser = new CompileTimeDependencyAnalyser(outputPath, outputFileName);

            if (verbose)
            {
                CTAssemAnalyser.AssemblyAnalysed += new CompileTimeDependencyAnalyser.AssemblyAnalysedEvent(AssemblyAnalysedEventHandler);
            }

            // run the analysis
            string ctResult = null;
            if (filePerAssembly)
            {
                foreach (string path in assemblyNamesAndPaths.Values)
                {
                    CTAssemAnalyser.AnalyseAssembly(path);
                }
            }
            else
            {
                ctResult = CTAssemAnalyser.AnalyseAssemblies(assemblyNamesAndPaths.Values);
            }

            if (ctResult != null)
            {
                Console.WriteLine(ctResult);
            }
            #endregion

            #region Dynamic Dependency Analysis
            if (runDynamicAnalysis)
            {
                Console.WriteLine();

                RuntimeCircularDependencyAnalyser RTCAssemAnalyser = new RuntimeCircularDependencyAnalyser();
                RTCAssemAnalyser.OutputFileName = outputFileName;

                if (verbose)
                {
                    RTCAssemAnalyser.AssemblyAnalysed += new RuntimeCircularDependencyAnalyser.AssemblyAnalysedEvent(AssemblyAnalysedEventHandler);
                }

                string rtcResult = null;
                if (filePerAssembly)
                {
                    foreach (string path in assemblyNamesAndPaths.Values)
                    {
                        RTCAssemAnalyser.AnalyseAssembly(path);
                    }
                }
                else
                {
                    rtcResult = RTCAssemAnalyser.AnalyseAssemblies(assemblyNamesAndPaths.Values);
                }

                if (rtcResult != null)
                {
                    Console.WriteLine(rtcResult);
                }
            }
            #endregion

            if (pauseAtCompletion)
            {
                Console.WriteLine("Press any key to quit...");
                Console.ReadKey(true);
            }
        }

        #region Static helper methods
        private static void AssemblyAnalysedEventHandler(object sender, AssemblyAnalysedEventArgs e)
        {
            Console.WriteLine(e.GetStringForConsole());
        }

        private static void ShowHelp(OptionSet optionSet)
        {
            Console.WriteLine("Finds the compile time dependencies of .NET assemblies, as well as some runtime dependencies, and saves the result in the GraphML format.");
            Console.WriteLine("CILADA can also look for circular dependencies in sets of assemblies.");
            Console.WriteLine();
            Console.WriteLine("Usage: cilada [OPTIONS] fullassemblypaths");
            Console.WriteLine(@"Example Usage: cilada -o 'C:\cilada\results' -n 'AnalysisResult' -d 'C:\Program Files\Kiwiplan' -r -i 'dll' 'C:\cilada\assemblyone.dll' 'C:\cilada\assemblytwo.dll' -p -a");
            Console.WriteLine();
            Console.WriteLine("Options:");
            optionSet.WriteOptionDescriptions(Console.Out);
            Console.WriteLine();
            Console.WriteLine("\tcilada " + Assembly.GetExecutingAssembly().GetName().Version);
        }

        private static bool ValidateUserOutputPath(string userOutputPath)
        {
            try
            {
                DirectoryInfo userOutputDir = new DirectoryInfo(userOutputPath);
                if (!userOutputDir.Exists)
                {
                    Directory.CreateDirectory(userOutputDir.FullName);
                }
                return new DirectoryInfo(userOutputDir.FullName).Exists;
            }
            catch (Exception e)
            {
                Console.WriteLine("Invalid output path because of: " + e.Message);
                return false;
            }
        }

        private static void ProcessIgnore(string ignoreValue, ref bool ignoreDlls, ref bool ignoreExes)
        {
            if (null == ignoreValue) return;
            if ("dll".Equals(ignoreValue.ToLower()))
            {
                ignoreExes = false;
                ignoreDlls = true;
                return;
            }
            if ("exe".Equals(ignoreValue.ToLower()))
            {
                ignoreExes = true;
                ignoreDlls = false;
                return;
            }

            Console.WriteLine("Invalid ignore value " + ignoreValue);
        }

        private static List<string> ProcessDirectories(List<string> dirs, bool recursive, bool ignoreDlls, bool ignoreExes)
        {
            List<string> assemblies = new List<string>();
            const string DLL = ".dll";
            const string EXE = ".exe";

            foreach (string d in dirs)
            {
                try
                {
                    log.Debug("Looking at directory " + d);
                    DirectoryInfo dir = new DirectoryInfo(d);
                    if (dir.Exists)
                    {
                        log.Debug(d + " exists");
                        foreach (FileInfo file in dir.GetFiles())
                        {
                            log.Debug("Found file " + file.FullName);
                            if (DLL.Equals(file.Extension) && !ignoreDlls)
                            {
                                assemblies.Add(file.FullName);
                            }
                            else if (EXE.Equals(file.Extension) && !ignoreExes)
                            {
                                assemblies.Add(file.FullName);
                            }
                        }
                        if (recursive)
                        {
                            List<string> subDirs = new List<string>();
                            foreach (DirectoryInfo subDir in dir.GetDirectories())
                            {
                                subDirs.Add(subDir.FullName);
                            }
                            assemblies.AddRange(ProcessDirectories(subDirs, recursive, ignoreDlls, ignoreExes));
                        }
                    }
                    else
                    {
                        Console.WriteLine("Directory " + d + " does not exist.");
                    }
                }
                catch (Exception e)
                {
                    Console.WriteLine("Directory " + d + " is not valid. " + e.Message + "\n" + e.StackTrace);
                }
            }

            return assemblies;
        }
        #endregion
    }
}
